Calibración de probabilidades

Introducción a machine learning

¿Qué es un modelo de clasificación?

Se busca predecir la probabilidad de ocurrencia de un evento a partir de ciertas características observables:

\(P(y=1) = f(X)\)

Siendo y una variable que puede tomar 2 valores: 0 o 1


Ejemplos de clasificación:

  • Titanic: Probabilidad de supervivencia
  • Iris: Clasificación de especies de plantas
  • German Credit: Probabilidad de default

Ejemplo: caso Titanic

Se consideran datos de OpenML: Titanic para ajustar un modelo que clasifique a individuos en sobrevivientes o no sobrevivientes del Titanic.

Código
data = datasets.fetch_openml(name="titanic", version=1)
y = data.target.astype("int")
X = (data.data[["age", "sex"]]
    # Construcción de dummy de sex=mujer
    .assign(d_mujer=lambda df: [1 if i == "female" else 0 for i in df["sex"]])
    .drop("sex", axis=1)
)

(pd.concat([y, X], axis=1)
    .head(10)
    .style.format(precision=2).hide(axis=0)
    .set_table_styles(styles)
)
Table 1: Datos de Titanic (muestra de 8 observaciones)
survived age d_mujer
1 29.00 1
1 0.92 0
0 2.00 1
0 30.00 0
0 25.00 1
1 48.00 0
1 63.00 1
0 39.00 0
1 53.00 1
0 71.00 0
Código
clf = DecisionTreeClassifier(max_depth=1).fit(X, y)
plt.figure(figsize=(4.5, 4.5))
plot_tree(
    clf,
    filled=True,
    impurity=False,
    feature_names=X.columns.tolist(),
    class_names=["Sobrevivió", "No sobrevivió"],
);
Figure 1: Modelo de árbol de decisión

A partir de los datos de la Table 1, el modelo de la Figure 1 (profundidad=1) utiliza la variable de género para clasificar a los tripulantes del Titanic según supervivencia. Según el modelo, las mujeres sobreviven y los hombres no.

Ejemplo (Cont.)

Ver código
y_pred = clf.predict(X)
ConfusionMatrixDisplay(
    confusion_matrix=confusion_matrix(y_true=y, y_pred=y_pred),
    display_labels=clf.classes_
).plot();

# Curva ROC
y_pred_proba = clf.predict_proba(X)[:, 1]
fpr, tpr, _ = roc_curve(y, y_pred_proba)
roc_display = RocCurveDisplay(fpr=fpr, tpr=tpr).plot()

Caso: German Credit

Datos

Código
df = (pd.read_csv(PATH_DATA)
  .drop('Unnamed: 0', axis=1)
  .assign(Risk = lambda x: np.where(x['Risk']=='good',0,1))
)
print(f"Se cuenta con un dataset de {df.shape[0]} observaciones y {df.shape[1]} variables \n")

df.sample(6, random_state=42).style.format(precision=2).hide(axis=0).set_table_styles(styles)
Se cuenta con un dataset de 1000 observaciones y 10 variables 
Table 2: Datos de German Credit (muestra de 6 observaciones)
Age Sex Job Housing Saving accounts Checking account Credit amount Duration Purpose Risk
24 female 2 own little little 3190 18 radio/TV 1
35 male 1 own moderate little 4380 18 car 0
32 male 2 own moderate little 2325 24 car 0
23 male 2 rent little rich 1297 12 radio/TV 0
35 male 3 own little nan 7253 33 car 0
64 male 1 rent little little 2384 24 radio/TV 0

Particiones

Partición en dataset de entrenamiento, validación y evaluación.

  • Dataset de entrenamiento –> Ajuste del modelo
  • Dataset de validación –> Calibración del modelo
  • Dataset de evaluación –> Métricas del modelo calibrado
Código
y = df[CLASE]
X = df.drop([CLASE], axis=1)

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.5, shuffle=True, stratify=y, random_state=42
)

# Lo ideal es utilizar la partición de validación para calibración
X_valid, X_test, y_valid, y_test = train_test_split(
    X_test, y_test, test_size=0.5, shuffle=True, stratify=y_test, random_state=42
)

print(f"N observaciones en entrenamiento: {X_train.shape[0]}")
print(f"N observaciones en validación: {X_valid.shape[0]}")
print(f"N observaciones en evaluación: {X_test.shape[0]}")
N observaciones en entrenamiento: 500
N observaciones en validación: 250
N observaciones en evaluación: 250

Preprocesamiento

Se construye un pipeline de preprocesamiento de variables, diferenciando el procesamiento de variables categóricas y numéricas

Ver código
numeric_transformer = Pipeline([
    ('impute', SimpleImputer(strategy='median')),
    ('scaler', MinMaxScaler())
])

categorical_transformer = Pipeline([
    ('ohe',OneHotEncoder(
        min_frequency=0.05,
        handle_unknown='infrequent_if_exist',
        sparse_output=False)
    )
])

preproc = ColumnTransformer([
    ('num', numeric_transformer,
      make_column_selector(dtype_include=['float','int'])),
    ('cat', categorical_transformer,
      make_column_selector(dtype_include=['object','category']))
], verbose_feature_names_out=False)
ColumnTransformer(transformers=[('num',
                                 Pipeline(steps=[('impute',
                                                  SimpleImputer(strategy='median')),
                                                 ('scaler', MinMaxScaler())]),
                                 <sklearn.compose._column_transformer.make_column_selector object at 0x14d7dd480>),
                                ('cat',
                                 Pipeline(steps=[('ohe',
                                                  OneHotEncoder(handle_unknown='infrequent_if_exist',
                                                                min_frequency=0.05,
                                                                sparse_output=False))]),
                                 <sklearn.compose._column_transformer.make_column_selector object at 0x14d7dd360>)],
                  verbose_feature_names_out=False)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

Modelado

Pipeline(steps=[('preproc',
                 ColumnTransformer(transformers=[('num',
                                                  Pipeline(steps=[('impute',
                                                                   SimpleImputer(strategy='median')),
                                                                  ('scaler',
                                                                   MinMaxScaler())]),
                                                  <sklearn.compose._column_transformer.make_column_selector object at 0x14d7dd480>),
                                                 ('cat',
                                                  Pipeline(steps=[('ohe',
                                                                   OneHotEncoder(handle_unknown='infrequent_if_exist',
                                                                                 min_frequency=0.05,
                                                                                 sparse_output=False))]),
                                                  <sklearn.compose._column_transformer.make_column_selector object at 0x14d7dd360>)],
                                   verbose_feature_names_out=False)),
                ('model',
                 HistGradientBoostingClassifier(class_weight={0: 1, 1: 1},
                                                max_depth=4, max_iter=1000,
                                                random_state=42))])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

Evaluación

              precision    recall  f1-score   support

           0       0.80      0.79      0.80       175
           1       0.53      0.53      0.53        75

    accuracy                           0.72       250
   macro avg       0.66      0.66      0.66       250
weighted avg       0.72      0.72      0.72       250

Calibración de probabilidades

Calibración de probabilidades

Se realiza la calibración del modelo (preajustado) mediante 2 métodos: sigmoide e isotónico. Notar que la probabilidad promedio predicha mediante los modelos claibrados se corresponde con el % de clase positiva observado en el dataset de entrenamiento.

Métodos

  • Calibración sigmoide
  • Calibración isotónica

Referencias:

Calibración sigmoide

Este método se basa en el modelo de regresión logística(Platt’s):

\(P(y_{i}=1) = \frac{1}{1+(e^{α+β(pred_{i})})}\)

Siendo: \(y_{i}\) la probabilidad calibrada para el individuo \(i\) \(pred_{i}\) el output del modelo no calibrado

En general este método es efectivo para muestras pequeñas o cuando el modelo no calibrado tiene errores de similares en predicciones de valores bajos y altos.

Referencias:

  • Platt, John. (2000). Probabilistic Outputs for Support Vector Machines and Comparisons to Regularized Likelihood Methods. Adv. Large Margin Classif, Volume 10.

Calibración sigmoide (Cont.)

Calibración isotónica

CalibratedClassifierCV(cv='prefit',
                       estimator=Pipeline(steps=[('preproc',
                                                  ColumnTransformer(transformers=[('num',
                                                                                   Pipeline(steps=[('impute',
                                                                                                    SimpleImputer(strategy='median')),
                                                                                                   ('scaler',
                                                                                                    MinMaxScaler())]),
                                                                                   <sklearn.compose._column_transformer.make_column_selector object at 0x14d7dd480>),
                                                                                  ('cat',
                                                                                   Pipeline(steps=[('ohe',
                                                                                                    OneHotEncoder(handle_unknown='infrequent_if_exist',
                                                                                                                  min_frequency=0.05,
                                                                                                                  sparse_output=False))]),
                                                                                   <sklearn.compose._column_transformer.make_column_selector object at 0x14d7dd360>)],
                                                                    verbose_feature_names_out=False)),
                                                 ('model',
                                                  HistGradientBoostingClassifier(class_weight={0: 1,
                                                                                               1: 1},
                                                                                 max_depth=4,
                                                                                 max_iter=1000,
                                                                                 random_state=42))]),
                       method='isotonic')
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
% observado: 0.3
Prob promedio: 0.308
Prob promedio (calibración sigmoide): 0.314
Prob promedio (calibración isotónica): 0.329

Comparación de modelos

ROC AUC modelo=0.744
ROC AUC modelo calibrado=0.744
Log loss modelo=0.929
Log loss modelo calibrado=0.531
Brier modelo=0.223
Brier modelo calibrado=0.177
Recall modelo=0.533
Recall modelo calibrado=0.400

Se visualizan las distribuciones de probabilidad predicha según el modelo calibrado y no calibrado.

Visualización

Tablas de ganancias

Se busca un modelo en donde la probabilidad promedio de cada bin se corresponda con el % de clase positiva observado en ese bin según las predicciones del modelo.

Esto es útil para la toma de decisiones, ya que permite establecer punto de cortes diferenciales en función de la aversión en riesgo de la entidad.

Por ejemplo:

  • En un modelo de scoring crediticio, otorgarle créditos a N individuos con probabilidad en cierto intervalo tiene un riesgo asociado (mora esperada)
  • En un modelo de churn (abandono) de clientes, otorgarle una promoción a individuos de cierto intervalo de probabilidad de abandono tiene un costo asociado (descuento para individuos que no abandonarían)
  • Entre otros.

Tabla de ganancias (Cont.)

Código
gain_table(preds=preds, bins=5)
Table 3: Tabla de ganancias (modelo no calibrado)
Bin N N_clase1 avg_prob perc_clase1 diff N_acum N_clase1_acum TPR
(-0.10%, 0.12%] 50 4 0.03% 8.00% 0.080 50 4 8.00%
(0.12%, 1.75%] 50 7 0.65% 14.00% 0.134 100 11 11.00%
(1.75%, 22.80%] 50 14 7.58% 28.00% 0.204 150 25 16.67%
(22.80%, 83.70%] 50 20 50.60% 40.00% 0.106 200 45 22.50%
(83.70%, 100.00%] 50 30 95.09% 60.00% 0.351 250 75 30.00%
Código
gain_table(preds=preds, pred_column='pred_sigmoid', bins=5)
Table 4: Tabla de ganancias (calibración sigmoide)
Bin N N_clase1 avg_prob perc_clase1 diff N_acum N_clase1_acum TPR
(1.20%, 14.10%] 50 4 8.69% 8.00% 0.007 50 4 8.00%
(14.10%, 22.80%] 50 7 18.47% 14.00% 0.045 100 11 11.00%
(22.80%, 35.00%] 50 14 28.12% 28.00% 0.001 150 25 16.67%
(35.00%, 49.90%] 50 20 41.46% 40.00% 0.015 200 45 22.50%
(49.90%, 80.70%] 50 30 60.25% 60.00% 0.003 250 75 30.00%